#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
扫描项目目录，按配置生成 3 份 Excel 报表并支持忽略路径。

1. 总体文件清单（大小单位 MB，降序）
2. 文件类型汇总
3. 按文件类型分 sheet 明细（多 sheet 工作簿）

支持在 scan_config.json 内配置 ignore_paths 数组，匹配时大小写不敏感，可含通配符 *。

TODO(张三, 2025-08-26): 支持命令行覆盖配置
"""

from __future__ import annotations

import fnmatch
import json
import time
from pathlib import Path
from typing import Dict, List, Sequence
import pandas as pd
import xlsxwriter


# ---------------- 日志 ----------------
def log(msg: str) -> None:
    print(f"[{time.strftime('%H:%M:%S')}] {msg}")


# ---------------- 数据结构 ----------------
class FileInfo:
    def __init__(self, file_path: Path) -> None:
        self.path = file_path
        self.name = file_path.name
        self.size_bytes = file_path.stat().st_size
        self.size_mb = self.size_bytes / 1024 / 1024

    @property
    def extension(self) -> str:
        return self.path.suffix.lower()


# ---------------- 配置读取 ----------------
def load_config(config_path: str = "scan_config.json") -> Dict:
    try:
        with open(config_path, "r", encoding="utf-8") as f:
            cfg = json.load(f)
    except Exception as e:
        log(f"读取配置失败: {e}")
        raise
    cfg.setdefault("ignore_paths", [])
    return cfg


# ---------------- 忽略判断 ----------------
def should_ignore(path: Path, ignore_patterns: Sequence[str], root: Path) -> bool:
    """
    判断给定路径是否应被忽略。
    支持相对 / 绝对路径、通配符 *，大小写不敏感。
    """
    str_path = str(path).lower()
    root_str = str(root).lower()

    for pat in ignore_patterns:
        pat_low = pat.lower()
        # 绝对路径模式
        if Path(pat_low).is_absolute():
            if fnmatch.fnmatch(str_path, pat_low):
                return True
        else:
            # 相对模式：把根目录拼上再匹配
            rel_abs = str((root / pat).absolute()).lower()
            if fnmatch.fnmatch(str_path, rel_abs) or str_path.startswith(rel_abs):
                return True
            # 直接相对路径匹配
            try:
                rel = path.relative_to(root)
                if fnmatch.fnmatch(str(rel).lower(), pat_low):
                    return True
            except ValueError:
                pass
    return False


# ---------------- 扫描 ----------------
def scan_directory(root: Path, max_size: int, ignore_patterns: Sequence[str]) -> List[FileInfo]:
    log(f"开始扫描目录：{root}")
    files: List[FileInfo] = []
    for entry in root.rglob("*"):
        if not entry.is_file():
            continue
        if should_ignore(entry, ignore_patterns, root):
            continue
        if entry.stat().st_size > max_size:
            continue
        files.append(FileInfo(entry))
    log(f"扫描完成，共 {len(files):,} 个文件符合要求")
    return files


# ---------------- 分组 ----------------
def build_type_map(files: List[FileInfo]) -> Dict[str, List[FileInfo]]:
    type_map: Dict[str, List[FileInfo]] = {}
    for f in files:
        ext = f.extension
        type_map.setdefault(ext, []).append(f)
    return type_map


# ---------------- Excel 输出 ----------------
def _write_df(df: pd.DataFrame, sheet_name: str, output_path: Path) -> None:
    with xlsxwriter.Workbook(str(output_path)) as wb:
        ws = wb.add_worksheet(sheet_name)
        for c, h in enumerate(df.columns):
            ws.write(0, c, h)
        for r, row in enumerate(df.values, start=1):
            for c, val in enumerate(row):
                ws.write(r, c, val)


def write_summary_sheet(files: List[FileInfo], output_path: Path) -> None:
    log("正在生成 1_总体文件清单.xlsx ...")
    files_sorted = sorted(files, key=lambda x: x.size_bytes, reverse=True)
    df = pd.DataFrame([
        {
            "序号": idx + 1,
            "文件名": f.name,
            "路径": str(f.path.absolute()),
            "大小(MB)": f.size_mb,
        }
        for idx, f in enumerate(files_sorted)
    ])
    _write_df(df, "总体文件清单", output_path)


def write_type_summary_sheet(type_map: Dict[str, List[FileInfo]], output_path: Path) -> None:
    log("正在生成 2_文件类型汇总.xlsx ...")
    rows = [
        {
            "序号": idx + 1,
            "文件类型": ext if ext else "<无扩展名>",
            "文件个数": len(files),
            "总大小(MB)": sum(f.size_mb for f in files),
        }
        for idx, (ext, files) in enumerate(type_map.items(), start=1)
    ]
    rows.sort(key=lambda x: x["总大小(MB)"], reverse=True)
    df = pd.DataFrame(rows)
    _write_df(df, "文件类型汇总", output_path)


def write_type_detail_sheets(type_map: Dict[str, List[FileInfo]], output_path: Path) -> None:
    log("正在生成 3_按类型分sheet明细.xlsx ...")
    headers = ["序号", "文件名", "路径", "大小(MB)"]
    with xlsxwriter.Workbook(str(output_path)) as wb:
        for ext, files in type_map.items():
            files_sorted = sorted(files, key=lambda x: x.size_bytes, reverse=True)
            sheet_name = ext.lstrip(".") if ext else "no_ext"
            sheet_name = sheet_name[:31]
            ws = wb.add_worksheet(sheet_name)
            for c, h in enumerate(headers):
                ws.write(0, c, h)
            for idx, f in enumerate(files_sorted, start=1):
                ws.write(idx, 0, idx)
                ws.write(idx, 1, f.name)
                ws.write(idx, 2, str(f.path.absolute()))
                ws.write(idx, 3, f.size_mb)


# ---------------- 主流程 ----------------
def main() -> None:
    cfg = load_config()
    root = Path(cfg["project_root"]).expanduser().resolve()
    max_size = cfg["max_file_size_bytes"]
    ignore_patterns = cfg["ignore_paths"]

    if not root.exists():
        log(f"项目路径不存在：{root}")
        return

    files = scan_directory(root, max_size, ignore_patterns)
    if not files:
        log("未找到任何符合要求的文件，报表未生成。")
        return

    type_map = build_type_map(files)
    output_dir = Path(__file__).resolve().parent / "output"
    output_dir.mkdir(exist_ok=True)

    write_summary_sheet(files, output_dir / "1_总体文件清单.xlsx")
    write_type_summary_sheet(type_map, output_dir / "2_文件类型汇总.xlsx")
    write_type_detail_sheets(type_map, output_dir / "3_按类型分sheet明细.xlsx")

    log(f"全部完成，输出目录：{output_dir}")


if __name__ == "__main__":
    main()